using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Text.RegularExpressions;

namespace SAInterfaceLibrary
{
    public class ProjectCreator
    {
        private ProjectCreationInfo _info;

        /// <summary>
        /// The info we use to create a project.
        /// </summary>
        public ProjectCreationInfo CreationInfo
        {
            get { return _info; }
            set { _info = value; }
        }

        /// <summary>
        /// Create the creator, and setup for actual project creation. Don't actually do
        /// any work, however.
        /// </summary>
        /// <param name="info"></param>
        public ProjectCreator(ProjectCreationInfo info)
        {
            _info = info;
        }

        private FileInfo _ide_location;

        /// <summary>
        /// Actually do the creation of the project!
        /// </summary>
        public FileInfo Create()
        {
            ///
            /// Output directory for the project
            /// 

            string ide_path = _info.ROOTSYSDirectory + "\\" + Configuraiton.Instance.SubdirectoryForIDE;
            if (!Directory.Exists(ide_path))
            {
                Directory.CreateDirectory(ide_path);
            }

            string ide_scripts = _info.ROOTSYSDirectory + "\\" + Configuraiton.Instance.SubdirectoryForScripts;
            if (!Directory.Exists(ide_scripts))
            {
                Directory.CreateDirectory(ide_scripts);
            }

            ///
            /// Get the location for the project IDE location.
            /// 

            string projectExtension = Path.GetExtension(Configuraiton.Instance.ProjectTempmlatePath);
            _ide_location = new FileInfo(ide_path + "\\root_build_project" + projectExtension);

            ///
            /// If a solution file already exists, delete it. We have to do this because
            /// the user might be changing the name of the configuration that they are using,
            /// and if the solution file contains a different name, but is called the same thing as
            /// this guy - well, that can really cause some problems.
            /// 

            FileInfo slnLocation = new FileInfo(ide_path + "\\root_build_project.sln");
            if (slnLocation.Exists)
            {
                slnLocation.Delete();
            }

            ///
            /// Now do the build files for each of the configuraitons
            /// 

            foreach (ProjectBuildConfigSpec spec in _info.ProjectBuildConfigs)
            {
                _config = spec;
                _config.AddEnvVar("ROOTSYS", _info.ROOTSYSDirectory);
                TemplateSubstitutionControl(Configuraiton.Instance.BuildBATTemplatePath, new FileInfo(ide_scripts + "\\build_" + spec.SafeName + ".bat"));
                TemplateSubstitutionControl(Configuraiton.Instance.ConfigBATTemplatePath, new FileInfo(ide_scripts + "\\configure_cmd_" + spec.SafeName + ".bat"));
                TemplateSubstitutionControl(Configuraiton.Instance.RebuildBATTemplatePath, new FileInfo(ide_scripts + "\\rebuild_" + spec.SafeName + ".bat"));
                TemplateSubstitutionControl(Configuraiton.Instance.CleanBATTemplatePath, new FileInfo(ide_scripts + "\\clean_" + spec.SafeName + ".bat"));
                TemplateSubstitutionControl(Configuraiton.Instance.SetupConfigSHTemplatePath, new FileInfo(ide_scripts + "\\setup_config_" + spec.SafeName + ".sh"));
                _config.SaveConfigToFile(ide_scripts + @"\spec.config_spec");
            }

            ///
            /// Are there any special files that need to be written out?
            /// 

            foreach (var f in Configuraiton.Instance.FilesToTranslate)
            {
                TemplateSubstitutionControl(f.TemplatePath, new FileInfo(ide_path + "\\" + f.FinalFileName));
            }


            ///
            /// Next, write out the settings file!
            /// 

            if (Configuraiton.Instance.UsesSettingsFile)
            {
                TemplateSubstitutionControl(Configuraiton.Instance.VCProjectSettingsTemplatePath, new FileInfo(_ide_location + ".user"));
            }

            ///
            /// Finally, do the project file. We do it after teh above config files so that it will pick those
            /// up and make them accessible directly from the IDE file.
            /// 

            TemplateSubstitutionControl(Configuraiton.Instance.ProjectTempmlatePath, _ide_location);

            ///
            /// Force config to be re-run
            /// 

            ExistingProjectInfo ei = new ExistingProjectInfo(_info.ROOTSYSDirectory);
            ei.RemoveOldConfigInfo();

            ///
            /// Return a pointer to the project
            /// 

            return _ide_location;
        }

        /// <summary>
        /// Run the template replacement process
        /// </summary>
        private void TemplateSubstitutionControl(string template_path, FileInfo finfo)
        {
            StreamReader reader = new StreamReader(template_path);
            StreamWriter writer = finfo.CreateText();

            ProcessTemplateFile(reader, writer);

            reader.Close();
            writer.Close();
        }

        /// <summary>
        /// Read from a template file, do call outs as appropriate to deal with new stuff.
        /// </summary>
        /// <param name="reader"></param>
        /// <param name="writer"></param>
        private void ProcessTemplateFile(StreamReader reader, StreamWriter writer)
        {
            ///
            /// Init things
            ///

            if (_find_macro_use == null)
            {
                _find_macro_use = new Regex(@"\$\((?<macroname>[^\)]*)\)");
            }

            ///
            /// Now look through file
            /// 

            foreach (string line in ReadFileLine(reader))
            {
                writer.WriteLine(TranslateLine(line));
            }
        }

        Regex _find_macro_use = null;

        /// <summary>
        /// Macro replace this line with whatever is in it.
        /// </summary>
        /// <param name="line"></param>
        /// <returns></returns>
        private string TranslateLine(string line)
        {
            MatchCollection mc = _find_macro_use.Matches(line);
            foreach (Match m in mc)
            {
                string replacement = TranslateMacro(m.Groups["macroname"].Value);
                line = line.Replace(m.Value, replacement);
            }

            return line;
        }

        /// <summary>
        /// This is a hack -- we need to pull out all of this template processing stuff! Yuck!
        /// </summary>
        private ProjectBuildConfigSpec _config = null;

        /// <summary>
        /// Actually translate a macro. If the macro starts with a "=" then if we can't translate it
        /// we replace it with the zero length string.
        /// 
        /// We are using $( - and it turns out vc2010 starts using this too. So we have to be careful to pass
        /// through things we don't know about!
        /// </summary>
        /// <param name="input_string"></param>
        /// <returns></returns>
        private string TranslateMacro(string input_string)
        {
            ///
            /// Is it a "?RB?" macro - this is a macro that should get a null replacement if we don't
            /// know how to translate it here in this guy.'
            ///

            bool isNullableMacro = false;
            if (input_string.StartsWith("?RB?")) {
                isNullableMacro = true;
                input_string = input_string.Substring(4);
            }

            ///
            /// First see if the particular config we are running with has something special to say
            /// 

            string initialResult = Configuraiton.Instance.TranslateMacro(input_string);
            if (initialResult != null)
            {
                return initialResult;
            }

            ///
            /// Now, do the default guys
            /// 

            if (input_string == "project_name")
            {
                return "ROOT Build";
            }
            else if (input_string == "project_files")
            {
                return BuildProjectFilelist();
            }
            else if (input_string == "project_files_2010")
            {
                return BuildProjectFilelist2010();
            }
            else if (input_string == "project_configurations")
            {
                return BuildConfigurations();
            }
            else if (input_string == "filter_item_groups")
            {
                return BuildProjectItemGroupList2010();
            }
            else if (input_string == "filter_files")
            {
                return BuildProjectItemFilterFiles2010();
            }
            else if (input_string == "project_configurations_2010")
            {
                return Build2010Configurations();
            }
            else if (input_string == "project_configuration_preamble")
            {
                return Build2010ConfigurationPreambles();
            }
            else if (input_string == "unix_environment")
            {
                return BuildPathEnvVars();
            }
            else if (input_string == "config_name")
            {
                return _config.SafeName;
            }
            else if (input_string == "config_name_space_escaped")
            {
                return _config.SafeName.Replace(" ", @"\ ");
            }
            else if (input_string == "root_directory")
            {
                return _info.ROOTSYSDirectory;
            }
            else if (input_string == "path_to_ide_files")
            {
                return Configuraiton.Instance.SubdirectoryForScripts;
            }
            else if (input_string == "config_args")
            {
                return _config.ConfigArguments.Replace("\\", "\\\\");
            }
            else if (input_string == "settings-configuration")
            {
                return BuildConfigurationSettings();
            }
            else if (input_string == "make_options")
            {
                return BuildMakeOptions();
            }
            else if (input_string == "env_variables")
            {
                return BuildEnvDefinitions();
            }

            ///
            /// Return the original... nothing else to do!?
            /// 

            if (isNullableMacro)
            {
                return "";
            }
            else
            {
                return "$(" + input_string + ")";
            }
        }

        /// <summary>
        /// Builds all the items we need to put into the PATH env var
        /// </summary>
        /// <returns></returns>
        private string BuildPathEnvVars()
        {
            List<string> path_items = new List<string>();
            path_items.Add(Configuraiton.Instance.UnixBinPath);
            path_items.Add(_info.ROOTSYSDirectory + "\\bin");

            StringBuilder bld = new StringBuilder();
            bool is_first = true;
            foreach (string item in path_items) {
                bld.AppendFormat("{0}{1}", is_first ? "" : ";", item);
                is_first = false;
            }
            return bld.ToString();
        }

        /// <summary>
        /// Return a list of make options for the command line to make.
        /// </summary>
        /// <returns></returns>
        private string BuildMakeOptions()
        {
            StringBuilder bld = new StringBuilder();

            if (_info.NumberOfParallelMakeJobs > 0)
            {
                bld.Append("-j" + _info.NumberOfParallelMakeJobs.ToString());
            }

            foreach (string arg in _config.MakeArguments)
            {
                bld.Append(" " + arg);
            }

            ///
            /// If " were used, we have to escape them or they don't get passed through
            /// correctly.
            /// 

            string result = bld.ToString();
            return result.Replace("\"", "\\\"");
        }

        /// <summary>
        /// Generate all environment variable definitions.
        /// </summary>
        /// <returns></returns>
        private string BuildEnvDefinitions()
        {
            StringBuilder bld = new StringBuilder();
            foreach (EnvironmentVariable e in _info.EnvironmentVariables)
            {
                bld.AppendFormat("set \"{0}={1}\"", e.Name, e.Value);
                bld.AppendLine();
            }
            foreach (EnvironmentVariable e in _config.EnvironmentVariables)
            {
                bld.AppendFormat("set \"{0}={1}\"", e.Name, e.Value);
                bld.AppendLine();
            }
            return bld.ToString();
        }

        private string BuildConfigurationSettings()
        {
            StringBuilder bld = new StringBuilder(1024);

            foreach (ProjectBuildConfigSpec spec in _info.ProjectBuildConfigs)
            {
                bld.AppendLine("<Configuration Name=\"" + spec.Name + "\">");
                bld.AppendLine("<DebugSettings");
                bld.AppendLine("  Environment=\"ROOTSYS=" + _info.ROOTSYSDirectory + "&#x0A;PATH=" + _info.ROOTSYSDirectory + "\\bin;$(PATH)\"");
                bld.AppendLine("  Command=\"$(TargetPath)\"");
                bld.AppendLine("/>");
                bld.AppendLine("</Configuration>");
            }

            return bld.ToString();
        }

        /// <summary>
        /// Return the complete list of files in Project format. Whee!
        /// </summary>
        /// <returns></returns>
        private string BuildProjectFilelist()
        {
            ///
            /// A string builder about 1/2 a gig big -- because that is probably what this is going to take!
            /// 

            StringBuilder bld = new StringBuilder(700 * 1024);

            ///
            /// Now, just walk through the complete file list, and recursively add files.
            /// 

            DirectoryInfo root_info = new DirectoryInfo(_info.ROOTSYSDirectory);
            GenerateFilelist(bld, _ide_location, root_info,
                s => "<File RelativePath=\"" + s + "\"/>",
                (s, parents, start) => start ? "<Filter Name=\"" + s + "\">" : "</Filter>" );

            return bld.ToString();
        }

        /// <summary>
        /// Return the list of files for this ROOT project by scanning the main directory
        /// </summary>
        /// <returns></returns>
        private string BuildProjectFilelist2010()
        {
            StringBuilder bld = new StringBuilder(700 * 1024);
            DirectoryInfo root_info = new DirectoryInfo(_info.ROOTSYSDirectory);
            GenerateFilelist(bld, _ide_location, root_info,
                s => "    <None Include=\"" + s + "\"/>",
                (s, parents, start) => "");
            return bld.ToString();
        }

        /// <summary>
        /// Builds the list of item groups that sit at the top of
        /// the FILTERS file.
        /// </summary>
        /// <returns></returns>
        private string BuildProjectItemGroupList2010()
        {
            StringBuilder bld = new StringBuilder(1024 * 200);
            DirectoryInfo root_info = new DirectoryInfo(_info.ROOTSYSDirectory);
            GenerateFilelist(bld, _ide_location, root_info,
                s => "",
                (s, parents, start) =>
                {
                    if (!start) return "";
                    StringBuilder bldinner = new StringBuilder(512);
                    bldinner.AppendFormat("    <Filter Include=\"{0}\">", parents + s);
                    bldinner.AppendLine();
                    bldinner.AppendFormat("      <UniqueIdentifier>{{{0}}}</UniqueIdentifier>", Guid.NewGuid().ToString());
                    bldinner.AppendLine();
                    bldinner.AppendFormat("    </Filter>");
                    return bldinner.ToString();
                });
            return bld.ToString();
        }

        /// <summary>
        /// This builds all the actual files that do the filtering to build the structure.
        /// </summary>
        /// <returns></returns>
        private string BuildProjectItemFilterFiles2010()
        {
            StringBuilder bld = new StringBuilder(1024 * 1024);
            DirectoryInfo root_info = new DirectoryInfo(_info.ROOTSYSDirectory);
            string currentParentDir = "";
            GenerateFilelist(bld, _ide_location, root_info,
                s =>
                {
                    StringBuilder bldinner = new StringBuilder(256);
                    bldinner.AppendFormat("    <None Include=\"{0}\">", s);
                    bldinner.AppendLine();
                    bldinner.AppendFormat("      <Filter>{0}</Filter>", currentParentDir);
                    bldinner.AppendLine();
                    bldinner.Append("    </None>");
                    return bldinner.ToString();
                },
                (s, parents, start) =>
                {
                    currentParentDir = parents + s;
                    return "";
                });
            return bld.ToString();
        }

        /// <summary>
        /// Callback to calc the string we need for the path callback.
        /// </summary>
        /// <param name="relpath"></param>
        /// <returns></returns>
        private delegate string RenderSingleFileCallback(string relpath);

        /// <summary>
        /// Renderst the start/end of a directory 
        /// </summary>
        /// <param name="relpath"></param>
        /// <param name="isStart">True if this is the start of a directory, false otherwise</param>
        /// <returns></returns>
        private delegate string RenderSingleFileCallbackDir(string subdirName, string parentDirs, bool isStart);

        /// <summary>
        /// Generate the file list recursively. This is used (normally) to start the whole thing off.
        /// </summary>
        /// <param name="bld"></param>
        /// <param name="prj_file"></param>
        /// <param name="root_info"></param>
        /// <param name="callbackFile"></param>
        /// <param name="callbackDir"></param>
        private void GenerateFilelist(StringBuilder bld, FileInfo prj_file, DirectoryInfo root_info,
            RenderSingleFileCallback callbackFile,
            RenderSingleFileCallbackDir callbackDir)
        {
            GenerateFilelist(bld, prj_file, root_info, callbackFile, "", callbackDir);
        }

        /// <summary>
        /// Recursively generate the filelist. :-)
        /// </summary>
        /// <param name="bld"></param>
        /// <param name="root_info"></param>
        /// <param name="subDirLocation">How many subdirs are we down from the initial directory here?</param>
        private void GenerateFilelist(StringBuilder bld, FileInfo prj_file, DirectoryInfo root_info,
            RenderSingleFileCallback callbackFile,
            string subDirLocation,
            RenderSingleFileCallbackDir callbackDir)
        {
            ///
            /// If we are starting down the path, well, we'd better keep seperators going!
            /// 

            if (subDirLocation != "")
            {
                subDirLocation = subDirLocation + "\\";
            }

            ///
            /// Add all the files
            /// 

            foreach (FileInfo finfo in root_info.GetFiles())
            {
                if (Configuraiton.Instance.FileIsGood(finfo))
                {
                    string line = callbackFile(CalcRelativePath(finfo, prj_file));
                    if (line.Length > 0)
                    {
                        bld.AppendLine(line);
                    }
                }
            }

            ///
            /// Look for sub directories
            /// 

            foreach (DirectoryInfo subdir in root_info.GetDirectories())
            {
                ///
                /// Some directories should not be done at all!
                /// 

                if (!Configuraiton.Instance.DirectoryIsGood(subdir))
                {
                    continue;
                }

                ///
                /// Create a sub folder in the IDE for this guy
                /// 

                string line = callbackDir(subdir.Name, subDirLocation, true);
                if (line.Length > 0)
                {
                    bld.AppendLine(line);
                }

                ///
                /// Do the subfolder
                /// 

                GenerateFilelist(bld, prj_file, subdir, callbackFile, subDirLocation + subdir.Name, callbackDir);

                ///
                /// Close off the filter
                /// 

                line = callbackDir(subdir.Name, subDirLocation, false);
                if (line.Length > 0)
                {
                    bld.AppendFormat(line);
                }
            }
        }

        /// <summary>
        /// Return the relative path to finfo from prj_file.
        /// </summary>
        /// <param name="finfo"></param>
        /// <param name="prj_file"></param>
        /// <returns></returns>
        private string CalcRelativePath(FileInfo finfo, FileInfo prj_file)
        {
            string[] finfo_directories = finfo.DirectoryName.Split('\\');
            string[] prj_file_directories = prj_file.DirectoryName.Split('\\');

            int common_count = 0;
            while (
                common_count < finfo_directories.Length
                && common_count < prj_file_directories.Length
                && finfo_directories[common_count] == prj_file_directories[common_count])
            {
                common_count = common_count + 1;
            }

            ///
            /// For each directory left in the prj_file we have to do a ".."
            /// 

            StringBuilder result = new StringBuilder();
            for (int c = common_count; c < prj_file_directories.Length; c++)
            {
                result.Append("..\\");
            }

            ///
            /// Now, dip down and in
            /// 

            for (int c = common_count; c < finfo_directories.Length; c++)
            {
                result.AppendFormat("{0}\\", finfo_directories[c]);
            }

            ///
            /// And finally the filename
            /// 

            result.Append(finfo.Name);
            return result.ToString();
        }

        private string BuildConfigurations()
        {
            StringBuilder bld = new StringBuilder(1024);
            foreach (ProjectBuildConfigSpec spec in _info.ProjectBuildConfigs)
            {
                bld.AppendLine("<Configuration");
                bld.AppendLine("  Name=\"" + spec.Name + "|Win32\"");
                bld.AppendLine("  OutputDirectory=\"bin\"");
                bld.AppendLine("  IntermediateDirectory=\"bin\"");
                bld.AppendLine("  ConfigurationType=\"0\"");
                bld.AppendLine("  >");
                bld.AppendLine("  <Tool");
                bld.AppendLine("    Name=\"VCNMakeTool\"");
                bld.AppendLine("    BuildCommandLine=\"&quot;" + Configuraiton.Instance.SubdirectoryForScripts + "\\build_" + spec.SafeName + ".bat&quot;\"");
                bld.AppendLine("    ReBuildCommandLine=\"&quot;" + Configuraiton.Instance.SubdirectoryForScripts + "\\rebuild_" + spec.SafeName + ".bat&quot;\"");
                bld.AppendLine("    CleanCommandLine=\"&quot;" + Configuraiton.Instance.SubdirectoryForScripts + "\\clean_" + spec.SafeName + ".bat&quot;\"");
                bld.AppendLine("    IncludeSearchPath=\"include\"");
                bld.AppendLine("    ForcedIncludes=\"build\\win32\\w32pragma.h\"");
                bld.AppendLine("    Output=\"bin\\root.exe\"");
                bld.AppendLine("    />");
                bld.AppendLine("</Configuration>");
            }
            return bld.ToString();
        }

        private string Build2010Configurations()
        {
            StringBuilder bld = new StringBuilder(1024 * _info.ProjectBuildConfigs.Count);
            foreach (ProjectBuildConfigSpec spec in _info.ProjectBuildConfigs)
            {
                bld.AppendLine("  <PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='" + spec.SafeName + "|Win32'\" Label=\"Configuration\">");
                bld.AppendLine("    <ConfigurationType>Makefile</ConfigurationType>");
                bld.AppendLine("  </PropertyGroup>");
                bld.AppendLine("  <PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='" + spec.SafeName + "|Win32'\">");
                bld.AppendLine("    <NMakeBuildCommandLine>" +  Configuraiton.Instance.SubdirectoryForScripts + "\\build_" + spec.SafeName + ".bat</NMakeBuildCommandLine>");
                bld.AppendLine("    <NMakeOutput>bin\\root.exe</NMakeOutput>");
                bld.AppendLine("    <NMakeCleanCommandLine>" + Configuraiton.Instance.SubdirectoryForScripts + "\\clean_" + spec.SafeName + ".bat</NMakeCleanCommandLine>");
                bld.AppendLine("    <NMakePreprocessorDefinitions></NMakePreprocessorDefinitions>");
                bld.AppendLine("    <NMakeReBuildCommandLine>" + Configuraiton.Instance.SubdirectoryForScripts + "\\clean_" + spec.SafeName + ".bat</NMakeReBuildCommandLine>");
                bld.AppendLine("  </PropertyGroup>");
            }

            return bld.ToString();
        }

        private string Build2010ConfigurationPreambles()
        {
            StringBuilder bld = new StringBuilder(1024);
            foreach (ProjectBuildConfigSpec spec in _info.ProjectBuildConfigs)
            {
                bld.AppendLine("    <ProjectConfiguration Include=\"" + spec.SafeName + "|Win32\">");
                bld.AppendLine("     <Configuration>" + spec.Name + "</Configuration>");
                bld.AppendLine("     <Platform>Win32</Platform>");
                bld.AppendLine("    </ProjectConfiguration>");
            }
            return bld.ToString();
        }


        /// <summary>
        /// Simpler iterator to make looping over the lines in a file easy.
        /// </summary>
        /// <param name="reader"></param>
        /// <returns></returns>
        private IEnumerable<string> ReadFileLine(StreamReader reader)
        {
            TextReader tr = (TextReader)reader;
            string line;
            while ((line = tr.ReadLine()) != null)
            {
                yield return line;
            }
        }
    }
}
